package com.xuecheng.orders.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradeQueryRequest;
import com.alipay.api.response.AlipayTradeQueryResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.utils.IdWorkerUtils;
import com.xuecheng.base.utils.QRCodeUtil;
import com.xuecheng.messagesdk.model.po.MqMessage;
import com.xuecheng.messagesdk.service.MqMessageService;
import com.xuecheng.orders.config.AlipayConfig;
import com.xuecheng.orders.config.PayNotifyConfig;
import com.xuecheng.orders.mapper.XcOrdersGoodsMapper;
import com.xuecheng.orders.mapper.XcOrdersMapper;
import com.xuecheng.orders.mapper.XcPayRecordMapper;
import com.xuecheng.orders.model.dto.AddOrderDto;
import com.xuecheng.orders.model.dto.PayRecordDto;
import com.xuecheng.orders.model.dto.PayStatusDto;
import com.xuecheng.orders.model.po.XcOrders;
import com.xuecheng.orders.model.po.XcOrdersGoods;
import com.xuecheng.orders.model.po.XcPayRecord;
import com.xuecheng.orders.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private XcOrdersMapper ordersMapper;
    @Autowired
    private XcOrdersGoodsMapper ordersGoodsMapper;
    @Autowired
    private XcPayRecordMapper payRecordMapper;
    @Autowired
    private OrderServiceImpl currentProxy;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private MqMessageService mqMessageService;



    @Value("${pay.alipay.APP_ID}")
    String APP_ID;
    @Value("${pay.alipay.APP_PRIVATE_KEY}")
    String APP_PRIVATE_KEY;
    @Value("${pay.alipay.ALIPAY_PUBLIC_KEY}")
    String ALIPAY_PUBLIC_KEY;
    @Value("${pay.qrcodeurl}")
    String qrcodeurl;

    @Override
    @Transactional
    //生成支付二维码
    public PayRecordDto createOrder(String userId, AddOrderDto addOrderDto) {

        //创建商品订单 也就是订单表 和 订单详情表
        XcOrders orders = saveXcOrders(userId, addOrderDto);
        if(orders==null){
            XueChengPlusException.cast("订单创建失败");
        }
        //防止重复支付 600002表示以支付
        if(orders.getStatus().equals("600002")){
            XueChengPlusException.cast("订单已支付");
        }
        //生成订单支付记录表 生成支付交易流水号 把支付交易流水号放到 订单支付表 里面 交易流水号用来给第三方的支付平台进行下单的
        XcPayRecord payRecord = createPayRecord(orders);
        Long payNo = payRecord.getPayNo();
        //QRCodeUtil在base工程下的二维码生成根据
        QRCodeUtil qrCodeUtil = new QRCodeUtil();
        //nginx的配置pay:  两个内网穿透 第一个在这：就是在这里进行的内网穿透 二维码就是一个网址 当扫描二维码就会请求这个网址 支付宝需要从定向到requestpay接口对支付宝进行下单
        //由于模拟器的沙箱支付宝不能调起支付的界面 所以采用手机沙箱进行支付 所以需要配置natapp上的内网穿透
        // qrcodeurl: http://64dk42.natappfree.cc/orders/requestpay?payNo=%s
        //用于生成支付二维码的URL地址 用户扫描该二维码即可跳转到支付宝支付页面完成支付操作 这个url就是支付订单服务的接口 requestpay 进行下单的地址
        String url = String.format(qrcodeurl, payNo);

        String qrCode = null;
        try {
            //url为以支付二维码的地址和支付流水号生成的二维码
            qrCode = qrCodeUtil.createQRCode(url, 200, 200);
        } catch (IOException e) {
            XueChengPlusException.cast("生成支付二维码出错");
        }

        PayRecordDto payRecordDto = new PayRecordDto();
        BeanUtils.copyProperties(payRecord,payRecordDto);
        payRecordDto.setQrcode(qrCode);
        return payRecordDto;
    }

    @Override
    public XcPayRecord getPayRecordByPayno(String payNo) {
        XcPayRecord xcPayRecord = payRecordMapper.selectOne(new LambdaQueryWrapper<XcPayRecord>()
                .eq(XcPayRecord::getPayNo, payNo));
        return xcPayRecord;
    }

    @Override
    public PayRecordDto queryPayResult(String payNo) {
        XcPayRecord payRecord = getPayRecordByPayno(payNo);
        if (payRecord == null) {
            XueChengPlusException.cast("请重新点击支付获取二维码");
        }
        //支付状态
        String status = payRecord.getStatus();
        //如果支付成功直接返回
        if ("601002".equals(status)) {
            PayRecordDto payRecordDto = new PayRecordDto();
            BeanUtils.copyProperties(payRecord, payRecordDto);
            return payRecordDto;
        }

        //从支付宝查询支付结果
        PayStatusDto payStatusDto = queryPayResultFromAlipay(payNo);
        //保存支付结果
        currentProxy.saveAliPayStatus(payStatusDto);

        //重新查询支付记录 更新支付结果
        payRecord = getPayRecordByPayno(payNo);
        PayRecordDto payRecordDto = new PayRecordDto();
        BeanUtils.copyProperties(payRecord, payRecordDto);
        return payRecordDto;

    }


    /**
     * 请求支付宝查询支付结果 由于被动收到的支付结果通知可能没有 所以在项目中还会有查询支付结果的接口
     * @param payNo 支付交易号
     * @return 支付结果
     */
    public PayStatusDto queryPayResultFromAlipay(String payNo){
        //========请求支付宝查询支付结果=============
        AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, APP_ID, APP_PRIVATE_KEY, "json", AlipayConfig.CHARSET, ALIPAY_PUBLIC_KEY, AlipayConfig.SIGNTYPE); //获得初始化的AlipayClient
        AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
        JSONObject bizContent = new JSONObject();
        //payNo是本系统的交易支付号也就是支付交易流水号 拿着这个支付号流水号就可以到第三方支付系统获取到支付的结果了
        bizContent.put("out_trade_no", payNo);
        request.setBizContent(bizContent.toString());
        AlipayTradeQueryResponse response = null;
        try {
            response = alipayClient.execute(request);
            if (!response.isSuccess()) {
                XueChengPlusException.cast("请求支付查询查询失败");
            }
        } catch (AlipayApiException e) {
            log.error("请求支付宝查询支付结果异常:{}", e.toString(), e);
            XueChengPlusException.cast("请求支付查询查询失败");
        }

        //获取支付结果
        String resultJson = response.getBody();
        //转map 由于支付宝返回的是json 所以需要把json转成map方便取出数据
        Map resultMap = JSON.parseObject(resultJson, Map.class);
        Map alipay_trade_query_response = (Map) resultMap.get("alipay_trade_query_response");
        //支付结果
        String trade_status = (String) alipay_trade_query_response.get("trade_status");
        String total_amount = (String) alipay_trade_query_response.get("total_amount");
        String trade_no = (String) alipay_trade_query_response.get("trade_no");
        //保存支付结果
        PayStatusDto payStatusDto = new PayStatusDto();
        //payNo支付交易流水号 也就是本系统的支付交易号
        //在支付结果里面有支付服务下单的订单号用于将来查询数据库的 支付记录表 拿到订单信息 更新订单状态
        payStatusDto.setOut_trade_no(payNo);
        //交易状态 判断订单的成功与否拿到这个结果可以对订单服务的订单表进行更新数据库的订单状态
        payStatusDto.setTrade_status(trade_status);
        payStatusDto.setApp_id(APP_ID);
        //支付宝的交易号可以作为交易的有据可依
        payStatusDto.setTrade_no(trade_no);
        payStatusDto.setTotal_amount(total_amount);
        return payStatusDto;

    }

    /**
     * @description 保存支付宝支付结果
     * @param payStatusDto  支付结果信息
     * @return void
     */
    @Transactional
    public void saveAliPayStatus(PayStatusDto payStatusDto){
        //支付流水号
        String payNo = payStatusDto.getOut_trade_no();
        //查询支付记录表的记录
        XcPayRecord payRecord = getPayRecordByPayno(payNo);
        if (payRecord == null) {
            XueChengPlusException.cast("支付记录找不到");
        }
        //支付结果
        String trade_status = payStatusDto.getTrade_status();
        log.debug("收到支付结果:{},支付记录:{}}", payStatusDto.toString(),payRecord.toString());
        if (trade_status.equals("TRADE_SUCCESS")) {

            //支付金额变为分 totalPrice是支付宝返回给我们的金额
            Float totalPrice = payRecord.getTotalPrice() * 100;
            //total_amount是用户实际下单的金额
            Float total_amount = Float.parseFloat(payStatusDto.getTotal_amount()) * 100;
            //校验是否一致
            if (!payStatusDto.getApp_id().equals(APP_ID) || totalPrice.intValue() != total_amount.intValue()) {
                //校验失败
                log.info("校验支付结果失败,支付记录:{},APP_ID:{},totalPrice:{}" ,payRecord.toString(),payStatusDto.getApp_id(),total_amount.intValue());
                XueChengPlusException.cast("校验支付结果失败");
            }
            log.debug("更新支付结果,支付交易流水号:{},支付结果:{}", payNo, trade_status);
            //这一部分就是拿到了支付结果然后进行对原本的支付记录表进行补充的 createPayRecord在这个方法之后对支付表进行补充
            XcPayRecord payRecord_u = new XcPayRecord();
            payRecord_u.setStatus("601002");//支付成功
            payRecord_u.setOutPayChannel("Alipay");
            payRecord_u.setOutPayNo(payStatusDto.getTrade_no());//支付宝交易号
            payRecord_u.setPaySuccessTime(LocalDateTime.now());//通知时间
            //根据本系统的支付流水号更新对应的支付记录表的数据 更新的内容为payRecord_u封装的OutPayNo
            int update1 = payRecordMapper.update(payRecord_u, new LambdaQueryWrapper<XcPayRecord>().eq(XcPayRecord::getPayNo, payNo));
            if (update1 > 0) {
                log.info("更新支付记录状态成功:{}", payRecord_u.toString());
            } else {
                log.info("更新支付记录状态失败:{}", payRecord_u.toString());
                XueChengPlusException.cast("更新支付记录状态失败");
            }
            //关联的订单号   因为支付记录表关联了订单表的id  所以可以根据这个关联修改订单表的支付状态 用于在流程图中选课记录判断该课程是否被支付
            Long orderId = payRecord.getOrderId();
            XcOrders orders = ordersMapper.selectById(orderId);
            if (orders == null) {
                log.info("根据支付记录[{}}]找不到订单", payRecord_u.toString());
                XueChengPlusException.cast("根据支付记录找不到订单");
            }
            XcOrders order_u = new XcOrders();
            order_u.setStatus("600002");//支付成功
            int update = ordersMapper.update(order_u, new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getId, orderId));
            if (update > 0) {
                log.info("更新订单表状态成功,订单号:{}", orderId);
            } else {
                log.info("更新订单表状态失败,订单号:{}", orderId);
                XueChengPlusException.cast("更新订单表状态失败");
            }
            //保存消息记录,参数1：支付结果通知类型，2: 业务id，3:业务类型
            //"payresult_notify"表示消息的类型为支付结果通知 getOutBusinessId为选课id
            //因为是广播的方式进行发送的消息 所以各个只要配置了消息的微服务都可以拿到消息
            // 所以需要放置OrderType表示让学习中心知道这个是选课的订单为购买课程 所以就进行消息的接收 其他微服务进行消息解析出来后就会进行丢弃
            //getOutBusinessId选课记录id 因为要更新选课的支付状态 所以在消息的发送方需要把选课记录的id发送过来 根据这个id查询出选课记录然后对这条选课记录的支付状态进行修改
            MqMessage mqMessage = mqMessageService.addMessage("payresult_notify", orders.getOutBusinessId(), orders.getOrderType(), null);
            //通知消息 支付服务要把消息通过消息队列通知到学习中心服务 需要跨微服务通过消息队列进行通知
            notifyPayResult(mqMessage);
        }

    }

    @Override
    public void notifyPayResult(MqMessage message) {

        //1、消息体，转json
        String msg = JSON.toJSONString(message);
        //设置消息持久化
        Message msgObj = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8))
                //PERSISTENT持久化
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
                .build();
        // 2.全局唯一的消息ID，需要封装到CorrelationData中
        //correlationData 功能:用于关联发送的消息与应用程序中的某个业务逻辑。它允许在发送消息时携带一个唯一标识符。
        //用途: 追踪消息的状态和结果，比如成功或失败的确认。
        CorrelationData correlationData = new CorrelationData(message.getId().toString());
        // 3.添加callback 走的是PayNotifyConfig里面的回调方法 消息发送成功才删除 失败的话就走的PayNotifyConfig里面的回调方法把失败的消息再次加入消息表
        correlationData.getFuture().addCallback(
                result -> {
                    assert result != null;
                    if(result.isAck()){
                        // 3.1.ack，消息成功
                        log.debug("通知支付结果消息发送成功, ID:{}", correlationData.getId());
                        //因为走了PayNotifyConfig里面的回调方法把消息重新加入了消息表 所以在这里只需要删除消息表中的记录
                        mqMessageService.completed(message.getId());
                    }else{
                        // 3.2.nack，消息失败
                        log.error("通知支付结果消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
                    }
                },
                //消息发送异常 也就是网络等其他原因出现了问题
                ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
        );
        //发送消息 因为convertAndSend是广播模式进行发送所以需要指定交换机
        //因为是广播模式 所以不需要路由key 空字符串表示对所有交换机都进行发送
        rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT, "", msgObj,correlationData);

    }

    //生成订单支付记录表 生成支付交易流水号 把支付交易流水号放到订单支付表里面
    //支付记录表有一些字段是需要等待支付宝支付成功后返回信息（拿到结果通知）才进行填写的
    // 主动查询的saveAliPayStatus在这个方法里面拿到支付的结果对支付记录表进行补充 还有就是被动查询的receivenotify也会进行补充 补充的东西都是一样的只是方式不同
    public XcPayRecord createPayRecord(XcOrders orders){
        Long ordersId = orders.getId();
        XcOrders xcOrders = ordersMapper.selectById(ordersId);

        if(xcOrders==null){
            XueChengPlusException.cast("订单不存在");
        }
        //以为了避免支付重复所以可以从订单中拿出状态判断订单是否支付过 支付了就不用生成支付记录了
        if(xcOrders.getStatus().equals("600002")){
            XueChengPlusException.cast("订单已支付");
        }
        XcPayRecord payRecord = new XcPayRecord();
        //payNo生成支付交易流水号 也就是本系统的支付交易号
        //IdWorkerUtils为base工程下面的工具类 流水号的生成方法有四种 本项目采用的雪华算法生成
        //因为每一次支付都会生成支付记录号 所以支付号不会重复 则就不会存在用户中途出现支付异常导致需要重新支付的情况
        long payNo = IdWorkerUtils.getInstance().nextId();
        payRecord.setPayNo(payNo);
        payRecord.setOrderId(xcOrders.getId());//商品订单号
        payRecord.setOrderName(xcOrders.getOrderName());
        payRecord.setTotalPrice(xcOrders.getTotalPrice());
        payRecord.setCurrency("CNY");
        payRecord.setCreateDate(LocalDateTime.now());
        payRecord.setStatus("601001");//未支付
        payRecord.setUserId(xcOrders.getUserId());
        int insert = payRecordMapper.insert(payRecord);
        if(insert<=0){
            XueChengPlusException.cast("插入订单记录表失败");
        }
        return payRecord;

    }
    //创建订单表和订单明细表
    @Transactional
    public XcOrders saveXcOrders(String userId,AddOrderDto addOrderDto){
        //幂等性处理
        //因为订单表的选课课程id的索引设置成了唯一约束 因为
        //所以对于一个课程的订单表的对象记录只能有一条 因为需要进行幂等性的判断（不管多少次操作结果都一样 一门课不能往订单表插入多条）同一门选课记录不能有多个订单
        //根据选课的id查询订单表
        XcOrders order = getOrderByBusinessId(addOrderDto.getOutBusinessId());
        if(order!=null){
            return order;
        }
        order = new XcOrders();
        //生成订单号 通过IdWorkerUtils的工具类根据雪花算法生成订单的id号
        long orderId = IdWorkerUtils.getInstance().nextId();
        order.setId(orderId);
        order.setTotalPrice(addOrderDto.getTotalPrice());
        order.setCreateDate(LocalDateTime.now());
        order.setStatus("600001");//未支付
        order.setUserId(userId);
        order.setOrderType(addOrderDto.getOrderType());
        order.setOrderName(addOrderDto.getOrderName());
        order.setOrderDetail(addOrderDto.getOrderDetail());
        order.setOrderDescrip(addOrderDto.getOrderDescrip());
        order.setOutBusinessId(addOrderDto.getOutBusinessId());//选课记录id
        //生成订单表数据
        ordersMapper.insert(order);
        //OrderDetail是一个json的数据格式 记录了商品的详情信息
        String orderDetailJson = addOrderDto.getOrderDetail();
        List<XcOrdersGoods> xcOrdersGoodsList = JSON.parseArray(orderDetailJson, XcOrdersGoods.class);
        xcOrdersGoodsList.forEach(goods->{
            XcOrdersGoods xcOrdersGoods = new XcOrdersGoods();
            BeanUtils.copyProperties(goods,xcOrdersGoods);
            xcOrdersGoods.setOrderId(orderId);//订单号
            //生成订单详情表数据 订单表和订单详情表是一对多的关系所以在循环里面进行了插入
            ordersGoodsMapper.insert(xcOrdersGoods);
        });
        return order;
    }


    //根据业务id查询订单
    public XcOrders getOrderByBusinessId(String businessId) {
        XcOrders orders = ordersMapper.selectOne(new LambdaQueryWrapper<XcOrders>().eq(XcOrders::getOutBusinessId, businessId));
        return orders;
    }

}
